<?php
/**
 * Tree structure model.
 * Tree algorythm is nested sets (http://en.wikipedia.org/wiki/Nested_set_model) 
 * 
 * @author Konstantin Shamko <konstantin.shamko@gmail.com> 
 * @version 0.0.1
 * @copyright  Copyright (c) 2009 Konstantin Shamko
 * @category VaselinEngine
 * @package Bel Classes
 * @license  New BSD License
 *
 */
abstract class Bel_Model_Tree extends Bel_Model_Abstract 
{
	/**
	 * Name of a table where tree is storing
	 *
	 * @var string
	 */
	protected $table;
	/**
	 * Name of a 'left_key' column (see nested set alg)
	 *
	 * @var string
	 */
	protected $table_left;
	/**
	 * Name of a 'right_key' column (see nested set alg)
	 *
	 * @var string
	 */
	protected $table_right;
	/**
	 * Name of a 'level' column (see nested set alg)
	 *
	 * @var string
	 */
	protected $table_level;
	/**
	 * Primary key of a 'tree' table
	 *
	 * @var string
	 */
	protected $table_id;
	/**
	 * Array with errors
	 *
	 * @var array
	 */
	protected $errors=array();
	/**
	 * Max available level in tree structure (0 - infinite). Set in constructor
	 *
	 * @var int
	 */
	protected $_maxlevel = 0;
	/**
	 * Additional fields in table (all except id, left_key, right_key and level)
	 *
	 * @var array
	 */
	protected $table_add_fields;
	/**
	 * Constructor
	 *
	 * @param array $array
	 */
	public function __construct($array=array()) {
		$this->_name = $this->table;
		$this->_primary = array($this->table_id);
		parent::__construct ( $array );
	}	
	/**
	 * Returns array of all tree nodes. This array contains: node id, node level, node left key, node right key,
	 * previous node id (with same level as current), next node id (with same level as current) and other data 
	 * (depends on your table)
	 *
	 * @param string $condition - condition string. use it to select data sets (see nested sets alg)
	 * @return array - tree data
	 */
	public function getTree($condition = ''){
		$select = $this->_db->select()
		->from(array('t1'=>$this->table),array('*','previous_id'=>new Zend_Db_Expr("(".$this->_db->select()->from(array('t2'=>$this->table),array($this->table_id))->where('t1.'.$this->table_level.'=t2.'.$this->table_level.' and t1.'.$this->table_left.' = t2.'.$this->table_right.'+1')->__toString().")"),'next_id'=>new Zend_Db_Expr("(".$this->_db->select()->from(array('t3'=>$this->table),array($this->table_id))->where('t1.'.$this->table_level.'=t3.'.$this->table_level.' and t1.'.$this->table_right.' = t3.'.$this->table_left.'-1')->__toString().")")))
		->order($this->table_left);
		if($condition!='') $select->where($condition);
		$stmt = $this->_db->query($select);
		$result = $stmt->fetchAll();

		return $result;
	}
	/**
	 * Returns array with specified node with it's all children
	 *
	 * @param int $node_id - id of a specified node
	 * @return array - branch data
	 */
	public function getTreeBranch($node_id,$addcond=''){
		$node_info=$this->getNodeInfo($node_id);
		
		if($node_info){
			$left_key = $node_info->{$this->table_left};
			$right_key = $node_info->{$this->table_right};
			$condition = $this->table_left.">= '".$left_key."' AND ".$this->table_right." <= '".$right_key."'";

			if($addcond!='') {
				$condition= "$condition AND $addcond";
			}
			return $this->getTree($condition);
		}
		return false;
	}
	/**
	 * Returns array with parent branch of a specified node
	 *
	 * @param int $node_id - id of a specified node
	 * @return array - branch data
	 */
	public function getParentBranch($node_id,$addcond=''){
		$node_info=$this->getNodeInfo($node_id);

		if(count($node_info)){
			$left_key = $node_info->{$this->table_left};
			$right_key = $node_info->{$this->table_right};
			$condition = $this->table_left.'<=\''.$left_key.'\' AND '.$this->table_right.'>=\''.$right_key.'\'';
			if($addcond!='') $condition= "($condition) AND ".$addcond;
			return $this->getTree($condition);
		}

		return false;
	}
	
	/**
	 * Adds new child node to specified parent node 
	 *
	 * @param int $parent_node - id of a parent node
	 * @param string $node_name - new node name
	 * @return int - new node id
	 */
	public function addNode($parent_node,$request){

		$node_info = $this->getNodeInfo($parent_node);

		//$left_key = $node_info[$this->table_left];
		$right_key = $node_info->{$this->table_right};
		$level = $node_info->{$this->table_level};

		$data=array(
		$this->table_right => new Zend_Db_Expr($this->table_right.'+2'),
		$this->table_left => new Zend_Db_Expr('CASE WHEN '.$this->table_left.' > '.$right_key.' THEN '.$this->table_left.' + 2 ELSE '.$this->table_left.' END')
		);

		$this->_db->update($this->table,$data,$this->table_right.'>='.$right_key);

		$data_to_add[$this->table_left] = $right_key;
		$data_to_add[$this->table_right] = $right_key+1;
		$data_to_add[$this->table_level] = $level+1;

		foreach ($this->table_add_fields as $field){
			$data_to_add[$field] = $request[$field];
		}

		$this->_db->insert($this->table, $data_to_add);

		return $this->_db->lastInsertid();
	}
	/**
	 * Removes node and all it's children
	 *
	 * @param int $node_id - node id
	 * @return bool - true if ok, otherwise false
	 */
	public function deleteNode($node_id){
		
		
		$node_info = $this->getNodeInfo($node_id);

		if(!$node_info){
			return true;
		};
		
		$left_key = $node_info->{$this->table_left};
		$right_key = $node_info->{$this->table_right};
		$level = $node_info->{$this->table_level};

		if($level == 1){
			$this->errors[] = 'cant_remove_root';
			return false;
		}

		$rowset = $this->fetchAll("$this->table_left >= $left_key AND $this->table_right <= $right_key");
		foreach ($rowset as $row){
			$row->delete();
		}
		
		$this->_db->query("UPDATE $this->table 
							SET 
							$this->table_left = CASE 
													WHEN $this->table_left > $left_key THEN $this->table_left - ($right_key - $left_key + 1)
							                     	ELSE $this->table_left
							                     END, $this->table_right = $this->table_right - ($right_key - $left_key + 1) WHERE $this->table_right > $right_key");

		return true;
	}
	
	/**
	 * Updates node record 
	 *
	 * @param integer $node_id - id of the entry to edit
	 * @param array $request - assosiative array with field=>value pairs
	 */
	public function editNode($node_id,$request){
		foreach ($this->table_add_fields as $field){
			if(isset($request[$field])){
				$data[$field] = $request[$field];
			}
		}
		$this->_db->update($this->table, $data,"$this->table_id='$node_id'");
	}	
	/**
	 * Return node info
	 *
	 * @param int $node_id - node id
	 * @return array - array with node data
	 */
	public function getNodeInfo($node_id){
		$select = $this->_db->select()
		->from($this->table)
		->where($this->table_id.' = ?',$node_id);

		$stmt = $this->_db->query($select);
		$result = $stmt->fetchAll();

		if(!count($result)) return false;
		
		return $result[0];
	}
	/**
    * Assigns a node with all its children to another parent.
    *
    * @param integer $ID node ID
    * @param integer $newParentId ID of new parent node
    * @param array $condition Array structure: array('and' => array('id = 0', 'id2 >= 3'), 'or' => array('sec = \'www\'', 'sec2 <> \'erere\'')), etc where array key - condition (AND, OR, etc), value - condition string
    * @return bool TRUE if successful, FALSE otherwise.
    */
	public function moveNode($ID, $newParentId, $condition = '') {
		$node_info = $this->getNodeInfo($ID);
		$leftId = $node_info->{$this->table_left};
		$rightId = $node_info->{$this->table_right};
		$level = $node_info->{$this->table_level};

		$node_info = $this->getNodeInfo($newParentId);
		$leftIdP = $node_info->{$this->table_left};
		$rightIdP = $node_info->{$this->table_right};
		$levelP = $node_info->{$this->table_level};

		if($this->_maxlevel && $levelP>$this->_maxlevel){
			$this->errors[] = 'cant_move_root';
			return false;
		}
		
		if($level==1){
			$this->errors[] = 'cant_move_root';
			return false;
		}

		if ($ID == $newParentId || $leftId == $leftIdP || ($leftIdP >= $leftId && $leftIdP <= $rightId) || ($level == $levelP+1 && $leftId > $leftIdP && $rightId < $rightIdP)) {
			//$this->ERRORS_MES[] = extension_loaded('gettext') ? _('cant_move_tree') : 'cant_move_tree';
			$this->errors[] = 'cant_move_node';
			return FALSE;
		}

		if (!empty($condition)) {
			$condition = $this->_PrepareCondition($condition);
		}

		if ($leftIdP < $leftId && $rightIdP > $rightId && $levelP < $level - 1) {
			$sql = 'UPDATE ' . $this->table . ' SET '
			. $this->table_level . ' = CASE WHEN ' . $this->table_left . ' BETWEEN ' . $leftId . ' AND ' . $rightId . ' THEN ' . $this->table_level.sprintf('%+d', -($level-1)+$levelP) . ' ELSE ' . $this->table_level . ' END, '
			. $this->table_right . ' = CASE WHEN ' . $this->table_right . ' BETWEEN ' . ($rightId+1) . ' AND ' . ($rightIdP-1) . ' THEN ' . $this->table_right . '-' . ($rightId-$leftId+1) . ' '
			. 'WHEN ' . $this->table_left . ' BETWEEN ' . $leftId . ' AND ' . $rightId . ' THEN ' . $this->table_right . '+' . ((($rightIdP-$rightId-$level+$levelP)/2)*2+$level-$levelP-1) . ' ELSE ' . $this->table_right . ' END, '
			. $this->table_left . ' = CASE WHEN ' . $this->table_left . ' BETWEEN ' . ($rightId+1) . ' AND ' . ($rightIdP-1) . ' THEN ' . $this->table_left . '-' . ($rightId-$leftId+1) . ' '
			. 'WHEN ' . $this->table_left . ' BETWEEN ' . $leftId . ' AND ' . $rightId . ' THEN ' . $this->table_left . '+' . ((($rightIdP-$rightId-$level+$levelP)/2)*2+$level-$levelP-1) . ' ELSE ' . $this->table_left . ' END '
			. 'WHERE ' . $this->table_left . ' BETWEEN ' . ($leftIdP+1) . ' AND ' . ($rightIdP-1);
		} elseif ($leftIdP < $leftId) {
			$sql = 'UPDATE ' . $this->table . ' SET '
			. $this->table_level . ' = CASE WHEN ' . $this->table_left . ' BETWEEN ' . $leftId . ' AND ' . $rightId . ' THEN ' . $this->table_level.sprintf('%+d', -($level-1)+$levelP) . ' ELSE ' . $this->table_level . ' END, '
			. $this->table_left . ' = CASE WHEN ' . $this->table_left . ' BETWEEN ' . $rightIdP . ' AND ' . ($leftId-1) . ' THEN ' . $this->table_left . '+' . ($rightId-$leftId+1) . ' '
			. 'WHEN ' . $this->table_left . ' BETWEEN ' . $leftId . ' AND ' . $rightId . ' THEN ' . $this->table_left . '-' . ($leftId-$rightIdP) . ' ELSE ' . $this->table_left . ' END, '
			. $this->table_right . ' = CASE WHEN ' . $this->table_right . ' BETWEEN ' . $rightIdP . ' AND ' . $leftId . ' THEN ' . $this->table_right . '+' . ($rightId-$leftId+1) . ' '
			. 'WHEN ' . $this->table_right . ' BETWEEN ' . $leftId . ' AND ' . $rightId . ' THEN ' . $this->table_right . '-' . ($leftId-$rightIdP) . ' ELSE ' . $this->table_right . ' END '
			. 'WHERE (' . $this->table_left . ' BETWEEN ' . $leftIdP . ' AND ' . $rightId. ' '
			. 'OR ' . $this->table_right . ' BETWEEN ' . $leftIdP . ' AND ' . $rightId . ')';
		} else {
			$sql = 'UPDATE ' . $this->table . ' SET '
			. $this->table_level . ' = CASE WHEN ' . $this->table_left . ' BETWEEN ' . $leftId . ' AND ' . $rightId . ' THEN ' . $this->table_level.sprintf('%+d', -($level-1)+$levelP) . ' ELSE ' . $this->table_level . ' END, '
			. $this->table_left . ' = CASE WHEN ' . $this->table_left . ' BETWEEN ' . $rightId . ' AND ' . $rightIdP . ' THEN ' . $this->table_left . '-' . ($rightId-$leftId+1) . ' '
			. 'WHEN ' . $this->table_left . ' BETWEEN ' . $leftId . ' AND ' . $rightId . ' THEN ' . $this->table_left . '+' . ($rightIdP-1-$rightId) . ' ELSE ' . $this->table_left . ' END, '
			. $this->table_right . ' = CASE WHEN ' . $this->table_right . ' BETWEEN ' . ($rightId+1) . ' AND ' . ($rightIdP-1) . ' THEN ' . $this->table_right . '-' . ($rightId-$leftId+1) . ' '
			. 'WHEN ' . $this->table_right . ' BETWEEN ' . $leftId . ' AND ' . $rightId . ' THEN ' . $this->table_right . '+' . ($rightIdP-1-$rightId) . ' ELSE ' . $this->table_right . ' END '
			. 'WHERE (' . $this->table_left . ' BETWEEN ' . $leftId . ' AND ' . $rightIdP . ' '
			. 'OR ' . $this->table_right . ' BETWEEN ' . $leftId . ' AND ' . $rightIdP . ')';
		}

		$this->_db->query($sql);

		return TRUE;
	}
	/**
    * Swapping nodes within the same level and limits of one parent with all its children: $id1 placed before or after $id2.
    *
    * @param integer $id1 first item ID
    * @param integer $id2 second item ID
    * @param string $position 'before' or 'after' $id2
    * @param array $condition Array structure: array('and' => array('id = 0', 'id2 >= 3'), 'or' => array('sec = \'www\'', 'sec2 <> \'erere\'')), etc where array key - condition (AND, OR, etc), value - condition string
    * @return bool TRUE if successful, FALSE otherwise.
    */
	public function ChangePositionAll($id1, $id2, $position = 'after') {
		$node_info = $this->getNodeInfo($id1);
		$leftId1 = $node_info->{$this->table_left};
		$rightId1 = $node_info->{$this->table_right};
		$level1 = $node_info->{$this->table_level};

		$node_info = $this->getNodeInfo($id2);
		$leftId2 = $node_info->{$this->table_left};
		$rightId2 = $node_info->{$this->table_right};
		$level2 = $node_info->{$this->table_level};

		if ($level1 <> $level2) {
			$this->errors[] = 'cant_change_position';
			return FALSE;
		}

		if($level1==1){
			$this->errors[] = 'cant_move_root';
			return false;
		}
	
		if ('before' == $position) {
			if ($leftId1 > $leftId2) {
				$sql = 'UPDATE ' . $this->table . ' SET '
				. $this->table_right . ' = CASE WHEN ' . $this->table_left . ' BETWEEN ' . $leftId1 . ' AND ' . $rightId1 . ' THEN ' . $this->table_right . ' - ' . ($leftId1 - $leftId2) . ' '
				. 'WHEN ' . $this->table_left . ' BETWEEN ' . $leftId2 . ' AND ' . ($leftId1 - 1) . ' THEN ' . $this->table_right . ' +  ' . ($rightId1 - $leftId1 + 1) . ' ELSE ' . $this->table_right . ' END, '
				. $this->table_left . ' = CASE WHEN ' . $this->table_left . ' BETWEEN ' . $leftId1 . ' AND ' . $rightId1 . ' THEN ' . $this->table_left . ' - ' . ($leftId1 - $leftId2) . ' '
				. 'WHEN ' . $this->table_left . ' BETWEEN ' . $leftId2 . ' AND ' . ($leftId1 - 1) . ' THEN ' . $this->table_left . ' + ' . ($rightId1 - $leftId1 + 1) . ' ELSE ' . $this->table_left . ' END '
				. 'WHERE ' . $this->table_left . ' BETWEEN ' . $leftId2 . ' AND ' . $rightId1;
			} else {
				$sql = 'UPDATE ' . $this->table . ' SET '
				. $this->table_right . ' = CASE WHEN ' . $this->table_left . ' BETWEEN ' . $leftId1 . ' AND ' . $rightId1 . ' THEN ' . $this->table_right . ' + ' . (($leftId2 - $leftId1) - ($rightId1 - $leftId1 + 1)) . ' '
				. 'WHEN ' . $this->table_left . ' BETWEEN ' . ($rightId1 + 1) . ' AND ' . ($leftId2 - 1) . ' THEN ' . $this->table_right . ' - ' . (($rightId1 - $leftId1 + 1)) . ' ELSE ' . $this->table_right . ' END, '
				. $this->table_left . ' = CASE WHEN ' . $this->table_left . ' BETWEEN ' . $leftId1 . ' AND ' . $rightId1 . ' THEN ' . $this->table_left . ' + ' . (($leftId2 - $leftId1) - ($rightId1 - $leftId1 + 1)) . ' '
				. 'WHEN ' . $this->table_left . ' BETWEEN ' . ($rightId1 + 1) . ' AND ' . ($leftId2 - 1) . ' THEN ' . $this->table_left . ' - ' . ($rightId1 - $leftId1 + 1) . ' ELSE ' . $this->table_left . ' END '
				. 'WHERE ' . $this->table_left . ' BETWEEN ' . $leftId1 . ' AND ' . ($leftId2 - 1);
			}
		}
		if ('after' == $position) {
			if ($leftId1 > $leftId2) {
				$sql = 'UPDATE ' . $this->table . ' SET '
				. $this->table_right . ' = CASE WHEN ' . $this->table_left . ' BETWEEN ' . $leftId1 . ' AND ' . $rightId1 . ' THEN ' . $this->table_right . ' - ' . ($leftId1 - $leftId2 - ($rightId2 - $leftId2 + 1)) . ' '
				. 'WHEN ' . $this->table_left . ' BETWEEN ' . ($rightId2 + 1) . ' AND ' . ($leftId1 - 1) . ' THEN ' . $this->table_right . ' +  ' . ($rightId1 - $leftId1 + 1) . ' ELSE ' . $this->table_right . ' END, '
				. $this->table_left . ' = CASE WHEN ' . $this->table_left . ' BETWEEN ' . $leftId1 . ' AND ' . $rightId1 . ' THEN ' . $this->table_left . ' - ' . ($leftId1 - $leftId2 - ($rightId2 - $leftId2 + 1)) . ' '
				. 'WHEN ' . $this->table_left . ' BETWEEN ' . ($rightId2 + 1) . ' AND ' . ($leftId1 - 1) . ' THEN ' . $this->table_left . ' + ' . ($rightId1 - $leftId1 + 1) . ' ELSE ' . $this->table_left . ' END '
				. 'WHERE ' . $this->table_left . ' BETWEEN ' . ($rightId2 + 1) . ' AND ' . $rightId1;
			} else {
				$sql = 'UPDATE ' . $this->table . ' SET '
				. $this->table_right . ' = CASE WHEN ' . $this->table_left . ' BETWEEN ' . $leftId1 . ' AND ' . $rightId1 . ' THEN ' . $this->table_right . ' + ' . ($rightId2 - $rightId1) . ' '
				. 'WHEN ' . $this->table_left . ' BETWEEN ' . ($rightId1 + 1) . ' AND ' . $rightId2 . ' THEN ' . $this->table_right . ' - ' . (($rightId1 - $leftId1 + 1)) . ' ELSE ' . $this->table_right . ' END, '
				. $this->table_left . ' = CASE WHEN ' . $this->table_left . ' BETWEEN ' . $leftId1 . ' AND ' . $rightId1 . ' THEN ' . $this->table_left . ' + ' . ($rightId2 - $rightId1) . ' '
				. 'WHEN ' . $this->table_left . ' BETWEEN ' . ($rightId1 + 1) . ' AND ' . $rightId2 . ' THEN ' . $this->table_left . ' - ' . ($rightId1 - $leftId1 + 1) . ' ELSE ' . $this->table_left . ' END '
				. 'WHERE ' . $this->table_left . ' BETWEEN ' . $leftId1 . ' AND ' . $rightId2;
			}
		}
		$this->_db->query($sql);
		return TRUE;
	}
	/**
	 * Return primary key field name
	 *
	 */
	public function getIdCol(){
		return $this->table_id;
	}	
	/**
	 * Return errors
	 *
	 * @return array - array with errors`
	 */
	public function getErrors(){
		return $this->errors;
	}	
}